<?php

abstract class dbModel extends abstractModel
{
	protected $rResult;						//result-id van een query, alleen databasemodels gebruiken dit
	protected $sCol_prefix	= '';			//prefix van tabelkolommen
	protected $sId_coll		= 'id';
	protected $sCond		= '';			//string with all the sql conditions
	protected $aSort		= Array();		//array with sorting coll's
	protected $aJoin_cond	= Array();		//array with conditions for joins
	protected $aCols		= Array();		//array with used coll's
	protected $aGroup		= Array();		//array with grouping colls
	protected $iSelect_limit;
	public static $runned = 0;
	public static $rHandler;
	
	
	public function __construct ($sSource = '')
	{
		parent::__construct($sSource);
		$this->iSelect_limit = MODEL_DEFAULT_LIMIT;
	}
	
	/**
	 * This method is called when you'll try to walk trough the object with for example an foreach loop
	 * @return		Void
	 */
	public function next ()
	{
		//zijn er nog elementen in de array NA de huidige pointer
		$this->bValid = (false !== next($this->aCache));
		
		//zijn we aan het eind van de cache-array en zijn er nog resultaten in de resultset
		if (!$this->bValid && $this->iCache < $this->iTotal)
		{
			//we gaan nog een resultaat uit de resultset ophalen en slaan het op in cache-array. Hierbij nemen we als key,
			//het record-id.
			$this->bValid	= true;
			$aResult		= mysql_fetch_assoc($this->rResult);
			
			/*foreach ($aResult as $sCol => $mData)
			{
				$aResult[$sCol] = ($mData);
			}*/
			
			/*if (isset($aResult[$this->sCol_prefix.'id']))
			{
				$this->aCache[$aResult[$this->sCol_prefix.'id']] = $aResult;
			}
			else
			{
				$this->aCache[] = $aResult;
			}*/
			$this->aCache[] = $aResult;
			
			$this->iCache++;
		}
	}
	
	
	/**
	 * Deze methode is verantwoordelijk voor het inladen van elementen in het model. Hierna kan dit object gebruikt worden
	 * als een array om door de elementen heen te lopen.
	 *
	 * @param	integer		$iStart			startnummer, bij het hoeveelste record moet worden begonnen met ophalen.
	 *										Deze parameter heeft alleen nut als er geen id is opgegeven. Defaultvalue: 0
	 * @param	integer		$iRows			aantal rijen, hoeveel records moeten er worden opgehaald.
	 *										Als de waarde '0' is wordt de constante MODEL_DEFAULT_LIMIT gebruikt.
	 *										Deze parameter heeft alleen nut als er geen id is opgegeven. Defaultvalue: 0
	 * @return	boolean						true als er meer dan 0 rijen terugkomen, anders false
	 */
	public function load ($iStart = 0, $iRows = -1)
	{
		//
		//query samenstellen
		//
		$this->oBench->start_mark();
		$sSql	= "SELECT %s FROM %s WHERE %s";
		$sCols	= (count($this->aCols) == 0) ? '*' : implode(', ', $this->aCols);
		$sWhere	= ($this->iId <= 0)	? "1=1" : $this->sSource.'.'.$this->sCol_prefix.$this->sId_coll ." = ". (int) $this->iId;
		$sWhere.= ' '.$this->sCond;
		$sFrom	= $this->sSource;
		
		//should there be any grouping...
		if (count($this->aGroup) > 0)
		{
			$sSql .= " GROUP BY ".implode(', ', $this->aGroup);
		}
		//should there be any sorting...
		if (count($this->aSort) > 0)
		{
			$sSql .= " ORDER BY ".implode(', ', $this->aSort);
		}
		//if $iRows is not a negative value, we add the LIMIT parameter to the query.
		//When $iRows is -1, there's no limit. When it's 0, the MODEL_DEFAULT_LIMIT will be used
		if ($iRows >= 0)
		{
			$iRows	 = ($iRows > 0) ? $iRows : $this->iSelect_limit;
			$sSql	.= " LIMIT %d, %d";
		}
		
		//
		//Adding many on one relations (m:1) to the query, so this modelsource has one relation with another source (through a foreign key).
		//The other source has mulptiple relations with this source (other source has a Primary Key). The data in this modelsource can't
		//exist without a relation to the other source.
		//
		//If $this->aBelongs_to is a string, it will be processed as $this->aBelongs_to = Array($this->aBelongs_to);
		//If $this->aBelongs_to is an array an you don't enter a key, the columnname 'id' will be used in the other source (with the PK).
		//If the key of an element is a string, this key will be the column in this source to wich the specified relation belongs to.
		//If the key of an element is an integer, the column in this source to wich the relation belongs to will be: relationtable_id
		//
		//Example (this source = sessions):
		//		Array('members')				turns to	FROM sessions, members WHERE sessions.members_id	= members.id
		//		Array('members.mid')			turns to	FROM sessions, members WHERE sessions.members_id 	= members.mid
		//		Array('mid' => 'members')		turns to	FROM sessions, members WHERE sessions.mid 			= members.id
		//		Array('mid' => 'members.mid')	turns to	FROM sessions, members WHERE sessions.mid 			= members.mid
		//
		if (is_string($this->aBelongs_to) && trim($this->aBelongs_to) != '')
		{
			$this->aBelongs_to = Array($this->aBelongs_to);
		}
		if (count($this->aBelongs_to) > 0)
		{
			//there are one or more relations
			foreach ($this->aBelongs_to as $iKey => $sRelation)
			{
				$aRelation	 = explode('.', $sRelation);
				$sFrom		.= ', '.$aRelation[0];
				
				$sCol_rel	 = isset($aRelation[1])	? $aRelation[1]			: 'id';		//relation column
				$sCol_source = is_numeric($iKey)	? $aRelation[0].'_id'	: $iKey;	//source column
				
				$sWhere		.= ' AND '.$aRelation[0].'.'.$sCol_rel.' = '.$this->sSource.'.'.$sCol_source;
			}
		}
		
		//
		//Adding one on many relations (1:m) to the query, so this modelsource has many relations with another source.
		//The other source has one relations with this source. The CAN be a relation, it's not obligated.
		//
		//If $this->aHas_many is a string, it will be processed as $this->aHas_many = Array($this->aHas_many);
		//If $this->aHas_many is an array an you don't enter a key, '{thistable}_id will be used as columnname in the other source.
		//If the key of an element is a string, this key will be the column to wich the specified relation belongs to.
		//If the key of an element is an integer, the column to wich the relation belongs to will be: relationtable_id
		//
		//Example (this source = members):
		//		Array('sessions')				turns to	FROM members LEFT OUTER JOIN sessions ON members.id		= sessions.members_id
		//		Array('sessions.mid')			turns to	FROM members LEFT OUTER JOIN sessions ON members.id		= sessions.mid
		//		Array('mid' => 'sessions')		turns to	FROM members LEFT OUTER JOIN sessions ON members.mid	= sessions.members_id
		//		Array('mid' => 'sessions.mid')	turns to	FROM members LEFT OUTER JOIN sessions ON members.mid	= sessions.mid
		//
		if (is_string($this->aHas_many) && trim($this->aHas_many) != '')
		{
			$this->aHas_many = Array($this->aHas_many);
		}
		if (count($this->aHas_many) > 0)
		{
			//there are one or more relations
			foreach ($this->aHas_many as $iKey => $sRelation)
			{
				$aRelation	 = explode('.', $sRelation);
				
				//when this table has a relationship with itself, we give one of the tables a different alias (TABLENAME2)
				$sAlias_rel	 = ($aRelation[0] == $this->sSource) ? $aRelation[0].'2' : $aRelation[0];
				
				//find out the columnnames
				$sCol_rel	 = isset($aRelation[1])	? $aRelation[1]				: $this->sSource.'_id';		//relation column
				$sCol_source = is_numeric($iKey)	? $this->sCol_prefix.'id'	: $iKey;					//source column
				
				$sFrom		.= ' LEFT OUTER JOIN '.$aRelation[0].' AS '.$sAlias_rel.' ON '.$this->sSource.'.'.$sCol_source.' = '.$sAlias_rel.'.'.$sCol_rel;
				$sFrom		.= (isset($this->aJoin_cond[$iKey])) ? ' '.$this->aJoin_cond[$iKey] : '';
			}
		}
		
		//creating the querystring
		$sSql = sprintf($sSql, $sCols, $sFrom, $sWhere, $iStart, $iRows);
		$this->oBench->add_mark('created query');
		
		//
		//query uitvoeren
		//
		$this->run_query($sSql);
		$this->oBench->start_mark();
		$this->iTotal = mysql_num_rows($this->rResult);
		$this->oBench->add_mark('counted rows');
		return $this->iTotal > 0;
	}
	
	
	public function add_cond ($sCond, $sType = 'AND')
	{
		$sType  = (strtoupper($sType) == 'OR') ? 'OR' : 'AND';
		if ($this->sCond == '' && $sType == 'OR')
		{
			//throw new errException(Array('or_cond'));
			//$this->sCond = $sCond;
			$sType = 'AND';
		}
		$this->sCond .= ' '.$sType.' '.$sCond;
	}
	
	
	public function add_simple_cond ($sCol, $sValue, $sOperator = '=', $sType = 'AND')
	{
		$sValue = $this->clean_up($sValue);
		if (!is_numeric($sValue))
		{
			$sValue = "'".$sValue."'";
		}
		$this->add_cond($this->clean_up($sCol).' '.$sOperator.' '.$sValue, $sType);
	}
	
	
	
	/**
	 * Add's the sort ascent parameter to query
	 *
	 * @param	String		$sColl		Collumn which you want to sort
	 * @return	Void
	 */
	public function sort ($sCond = '')
	{
		if ($sCond != '')
		{
			$this->aSort[] = $sCond.' ASC';
		}
	}
	
	
	/**
	 * Add's the sort descent parameter to query
	 *
	 * @param	String		$sColl		Collumn which you want to sort
	 * @return	Void
	 */
	public function asort ($sColl = '')
	{
		if ($sColl != '')
		{
			$this->aSort[] = $sColl.' DESC';
		}
	}
	
	
	/**
	 * Deze methode mag alleen intern aangeroepen worden en zorgt ervoor dat alle wijzigingen in een updatequery worden gemikt.
	 * @return	boolean
	 */
	protected function update ()
	{
		if (!$this->iState === self::UPDATE)
		{
			return false;
		}
		$this->oBench->start_mark();
		$aKeys		= Array();		//hierin slaan we allemaal arrays op met eventuele dubbele waardes
		$aCache		= Array();		//hierin slaan we alle unieke waardes op uit de cache-array
		$bReturn	= true;
		$sSql_base	= "UPDATE %s SET %s WHERE %s IN(%s)";
		
		//
		//We gaan de dubbele waardes uit de cache-array halen zodat we records met dezelfde updates in 1x keer kunnen updaten
		//
		foreach ($this->aCache as $iId => $aValue)
		{
			$iId = is_numeric($iId) ? (int) $iId : $iId;
			
			//we gaan kijken of er al eerder dezelfde array in de cache array is opgeslagen.
			//we willen namelijk bij eenzelfde update voor meerdere queries, maar 1 query uitvoeren!
			if (false !== $iCache_id = array_search($aValue, $aCache, true))
			{
				$iCache_id	= is_numeric($iCache_id) ? (int) $iCache_id : $iCache_id;
				$aKeys[$iCache_id][] = $iId;
			}
			else
			{
				$aKeys[]	= Array($iId);
				$aCache[]	= $aValue;
			}
		}
		$this->oBench->add_mark('filtered cache');
		
		//
		//elk record dat is gewijzigd gaan we updaten (meestal zal dit er maar 1 zijn)
		//
		foreach ($aCache as $iKey => $aValue)
		{
			$aCols	= array_keys($aValue);
			//dit ziet er ingewikkeld uit, maar is het niet. We maken eerst een nieuwe array met 'array_map', die door alle
			//elementen loopt met de functie 'make_query_item', en vervolges imploderen we die
			$sValues= implode(array_map(Array($this, 'make_query_item'), $aCols, $aValue), ', ');
			
			$sIds	= "'" . implode($aKeys[$iKey], ', ') . "'";
			$sSql	= sprintf($sSql_base, $this->sSource, $sValues, $this->sCol_prefix.$this->sId_coll, $sIds);
			$this->oBench->add_mark('created query');
			
			//de query uitvoeren
			$this->run_query($sSql);
			if (mysql_affected_rows(self::$rHandler) == 0)
			{
				$bReturn = false;
			}
		}
		$this->oBench->add_mark('runned '.count($this->aCache).' queries');
		$this->trashcan();
		
		//deze functie hebben we alleen hier nodig en zorgt ervoor dat we van 2 arrays, 1 string maken.
		//de eerste array bevat alle kolomnamen, de tweede bevat de waarde voor die kolom
		
		return $bReturn;
	}
	protected function make_query_item ($sCol, $sValue)
	{
		return $sCol."='".$sValue."'";
	}
	
	
	/**
	 * Deze methode mag alleen intern aangeroepen worden en zorgt ervoor dat alle nieuwe items uit de cache-array
	 * in een insertquery worden gemikt.
	 * @return	boolean
	 */
	protected function insert ()
	{
		if (!$this->iState === self::INSERT)
		{
			return false;
		}
		$this->oBench->start_mark();
		$bReturn	= true;
		$iId_new	= 0;
		$sSql_base	= "INSERT INTO %s (%s) VALUES (%s)";							//de basisopzet van de insertquery
		
		//
		//elk item uit de cache array gaan we inserten in de database
		//
		foreach ($this->aCache as $iId => $aValue)
		{
			$sCols		= implode(array_keys($aValue), ', ');						//de kolomnamen scheiden dmv een komma: kolom1, kolom2
			$sValues	= "'".implode($aValue, "', '")."'";							//de waardes scheiden door een komma: 'waarde1', 'waarde2'
			$sSql		= sprintf($sSql_base, $this->sSource, $sCols, $sValues);	//de query in elkaar zetten
			$this->oBench->add_mark('created query');
			
			//de query uitvoeren
			$this->run_query($sSql);
			if (mysql_affected_rows(self::$rHandler) == 0)
			{
				$bReturn = false;
			}
			
			//op de juiste manier weer in de cache gooien
			unset($this->aCache[$iId]);
			$iId_new				= (int) mysql_insert_id();
			$this->aCache[$iId_new]	= $aValue;
		}
		$this->oBench->add_mark('runned '.count($this->aCache).' queries');
		
		$this->trashcan();
		
		return $iId_new;
	}
	
	
	/**
	 * Deze methode mag alleen intern aangeroepen worden en zorgt ervoor dat alle items die in de cache-array staan
	 * in een deletequery worden gemikt.
	 * @return	boolean
	 */
	protected function delete ()
	{
		if (!$this->iState === self::DELETE)
		{
			return false;
		}
		$this->oBench->start_mark();
		echo sprint_r($this->aCache);
		//
		//elk item uit de cache array gaan we verwijderen. Hiervoor hebben we eigenlijk alleen de key uit de cache array nodig.
		//
		$sSql	= "DELETE FROM %s WHERE %s IN (%s)";										//de basisopzet van de deletequery
		$sCols	= "'" . implode(array_keys($this->aCache), ', ') . "'";
		$sSql	= sprintf($sSql, $this->sSource, $this->sCol_prefix.$this->sId_coll, $sCols);
		$this->oBench->add_mark('created query');
		
		$this->trashcan();
		
		//de query uitvoeren
		$this->run_query($sSql);
		return mysql_affected_rows(self::$rHandler) > 0;
	}
	
	
	/**
	 * Deze methode zorgt voor alle standaard rompslomp bij het uitvoeren van een query
	 * @return		boolean			true als het gelukt is, anders gooit die een dbException
	 */
	protected function run_query ($sSql)
	{
		$this->oBench->start_mark();
		$this->rResult	= @mysql_query($sSql, self::$rHandler);
		//echo "<br>".$sSql."<br>";
		if ($this->rResult === false)
		{
			die(mysql_error(self::$rHandler).' sql: '.$sSql);
		}
		//engine::start()->add_result($sSql);
		self::$runned++;
		$this->oBench->add_mark('runned query');
		return true;
	}
	
	
	public function offsetUnset ($mKey)
	{
		parent::offsetUnset((int) $mKey);
	}
	
	
	public function clean_up ($mValue)
	{
		if (is_array($mValue))
		{
			foreach ($mValue as $mId => $mRow)
			{
				$mValue[$mId] = $this->clean_up($mRow);
			}
		}
		else
		{
			$mValue = (mysql_real_escape_string($mValue));
		}
		
		return $mValue;
	}
	 
	
	public function search ($sKeywords, $aCols = Array())
	{
		if (is_string($aCols))
		{
			$aCols = Array($aCols);
		}
		
		//make searchstring with all keywords
		$sKeywords = $this->clean_up($sKeywords);
		$aKeywords = explode(" ", $sKeywords);
		
		$aSearch_arr = Array();
		foreach ($aKeywords as $sKeyword)
		{
			$aSearch_arr[] = "__column__ LIKE '%".$sKeyword."%'"; 
		}
		
		//create conditions
		foreach ($aCols as $sColumn)
		{
			$this->add_cond(str_replace("__column__", $sColumn, implode(" OR ", $aSearch_arr)), "OR");
		}
		//$this->aCols = $aCols;
	}
}

?>